﻿using System;
using System.Collections.Generic;
using T0yK4T.IO;
using T0yK4T.Tools;
using System.Collections;

namespace ToyChat.Server
{
    /// <summary>
    /// This class handles users!
    /// <para/>
    /// Basically all it does is keep track of who is online and who is not.
    /// <para/>
    /// Can be used to kick users
    /// </summary>
    public class UserHandler : ComponentBase, IUserHandler
    {
        private IServer owner;
        private Dictionary<string, IUser> users = new Dictionary<string, IUser>(); // Has format Username / Nick, User
        private Dictionary<int, string> idToUsername = new Dictionary<int, string>();
        private AssemblyLoader loader;
        private IEnumerable<ILoginModule> loginModules;
        private int nextUserID = int.MinValue;
        private static readonly string[] excludeAssemblies = new string[] { "MongoDB.dll", "MongoDB.Bson.dll", "MongoDB.Driver.dll", "protobuf-net.dll", "Skeinfish.dll", "T0yK4T.dll" };

        /// <summary>
        /// Initializes a new instance of UserHandler
        /// </summary>
        /// <param name="server">The server that this userhandler belongs to</param>
        public UserHandler(IServer server)
        {
            this.owner = server;
            this.loader = new AssemblyLoader();
            //this.loader = new ModuleLoader();
            base.LogInformation("Loading Login Modules");
            this.loginModules = this.loader.LoadDirectory<ILoginModule>(Environment.CurrentDirectory, excludeAssemblies);
            //base.LogInformation("Got {0} Login Modules", this.loginModules.Count);
        }

        /// <summary>
        /// Attempts to add a user connecting on the specified connection
        /// <para/>
        /// If the user is authenticated - OR - registered using a loaded <see cref="ILoginModule"/>
        /// <para/>
        /// the <paramref name="user"/> parameter will be set to the resulting <see cref="IUser"/> object
        /// </summary>
        /// <param name="connection">The connection that should be used</param>
        /// <param name="user">This paramter will be set to a <see cref="IUser"/> object retrieved from a <see cref="ILoginModule"/> upon user logon / registration</param>
        /// <returns>True if the user was succesfully authenticated or registered</returns>
        public bool AddUser(CryptoConnection connection, out IUser user)
        {
            if ((connection.ConnectionFlags & CryptoConnectionFlags.ManualRead) != CryptoConnectionFlags.ManualRead)
                throw new InvalidOperationException("CryptoConnection must be in manual read mode in order to add a user");
            try
            {
                bool authenticated = false;
                IUserInfo info = null;
                foreach (ILoginModule module in this.loginModules)
                {
                    if (authenticated = module.Authenticate(connection, this.owner.Settings.RequireLogin, this.owner.GlobalDBHandler, out info))
                        break;
                }
                if (authenticated && info != null && !this.users.ContainsKey(info.Username))
                {
                    user = this.EndAddUser(connection, info, info.Nickname);
                    return true;
                }
            }
            catch { }
            user = null;
            connection.Close();
            return false;
        }

        /// <summary>
        /// Called internally from <see cref="AddUser"/> upon completion
        /// <para/>
        /// This method sends the type aliases of the server
        /// <para/>
        /// and adds a new user object to the user table
        /// </summary>
        /// <param name="connection">The connection to send aliases on</param>
        /// <param name="info">The <see cref="IUserInfo"/> that was generated for the new user</param>
        /// <param name="nick">The nick name of the user that should be added</param>
        private IUser EndAddUser(CryptoConnection connection, IUserInfo info, string nick)
        {
            this.nextUserID++;
            if (this.nextUserID == -1 || this.nextUserID == 0) // UserID -1 is server!
                this.nextUserID = 1;
            User user = new User(connection, this.owner, info, nextUserID) { Nick = nick };
            this.users.Add(info.Username, user);
            this.idToUsername.Add(user.UserID, user.Username);


            AliasSpecifier sp = new AliasSpecifier { Aliases = this.owner.IDMap };
            Packet packet = new Packet((uint)InternalPacketTypes.TypeAliases);
            packet.SerializeData(sp);
            packet.ChannelID = -1;
            user.Send(packet);
            return user;
        }

        /// <summary>
        /// Removes the user with the username specified in <paramref name="info"/>
        /// <para/>
        /// No further action is taken
        /// </summary>
        /// <param name="info">The <see cref="IUserInfo"/> that represents the user that should be removed</param>
        public void RemoveUser(IUser info)
        {
            this.users.Remove(info.Username);
        }

        /// <summary>
        /// Kicks the specified user and broadcasts a kick message with no reason
        /// </summary>
        /// <param name="user">The user to kick</param>
        public void KickUser(IUser user)
        {
            KickUserMessage msg = new KickUserMessage { Nick = user.Nick };
            Packet pkt = new Packet { ChannelID = -1, TypeID = (uint)InternalPacketTypes.Kick, DestinationUserID = user.UserID };
            pkt.SerializeData(msg);
            user.Send(pkt);
            user.Disconnect();
        }

        /// <summary>
        /// Attempts to find a user with the specified user id
        /// </summary>
        /// <param name="userID">The User ID to look for</param>
        /// <param name="user">If succesful, will be set to an instance of <see cref="IUser"/> with the specified <paramref name="userID"/> </param>
        /// <returns>True if succesful, otherwise false</returns>
        public bool TryGetUser(int userID, out IUser user)
        {
            string username = string.Empty;
            if (this.idToUsername.TryGetValue(userID, out username))
                return this.TryGetUser(username, out user);
            else
            {
                user = null;
                return false;
            }
        }

        /// <summary>
        /// Attempts to find a user with the specified username
        /// </summary>
        /// <param name="username">The username to look for</param>
        /// <param name="user">If succesful, will be set to an instance of <see cref="IUser"/> with the specified <paramref name="username"/> </param>
        /// <returns>True if succesful, otherwise false</returns>
        public bool TryGetUser(string username, out IUser user)
        {
            return this.users.TryGetValue(username, out user);
        }

        /// <summary>
        /// Gets an instance of <see cref="IUser"/> that reprents an authenticated user with the specified username
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public IUser this[string username]
        {
            get
            {
                return this.users[username];
            }
        }

        /// <summary>
        /// Gets an instance of <see cref="IUser"/> that represents an authenticated user with the specified UserID
        /// </summary>
        /// <param name="userID"></param>
        /// <returns></returns>
        public IUser this[int userID]
        {
            get
            {
                return this.users[this.idToUsername[userID]];
            }
        }

        /// <summary>
        /// Gets an <see cref="IEnumerator{IUser}"/> that can be used to iterate through the currently authenticated users
        /// </summary>
        /// <returns>an instance of <see cref="IEnumerator{IUser}"/></returns>
        public IEnumerator<IUser> GetEnumerator()
        {
            return this.users.Values.GetEnumerator();
        }

        /// <summary>
        /// Gets an <see cref="IEnumerator"/> that can be used to iterate through the currently authenticated users
        /// </summary>
        /// <returns>an instance of <see cref="IEnumerator"/> that can be used to iterate through the usercollection</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.users.Values.GetEnumerator();
        }
    }
}